
本节中，将回顾C++实现支持包含模型的方式。所有这些实现都依赖于两个经典组件:编译器和链接器。编译器将源代码转换为目标文件，目标文件包含带有符号注释的机器码(交叉引用其他目标文件和库)。链接器通过组合目标文件并解析包含的符号交叉引用来创建可执行程序或库。接下来的内容中，假设有这样一个模型，尽管完全可能(但不流行)以其他方式实现C++。例如，可以想象这样一个C++解释器。

当在多个翻译单元中使用类模板特化时，编译器将在每个翻译单元中重复实例化过程。这没什么问题，因为类定义不会直接创建低层代码。它们仅在C++实现内部使用，用于验证和解释各种其他表达式和声明。这方面，类定义的多个实例化与类定义的多个包含(通常是通过包含头文件)在各种翻译单元中没有本质区别。

但若实例化一个(非内联)函数模板，情况可能会有所不同。若要为普通的非内联函数提供多个定义，就会违反ODR。例如，编译并链接一个由以下两个文件组成的程序:

\begin{lstlisting}[style=styleCXX]
// a.cpp:
int main()
{ }

// b.cpp:
int main()
{ }
\end{lstlisting}

C++编译器会分别编译模块不会有问题，因为确实是有效的C++单元。但若试图将两者链接在一起，链接器很可能会抗议——不允许重复定义。

相比之下，考虑模板的情况:

\begin{lstlisting}[style=styleCXX]
// t.hpp:
// common header (inclusion model)
template<typename T>
class S {
	public:
	void f();
};

template<typename T>
void S::f() // member definition
{ }
void helper(S<int>*);

// a.cpp:
#include "t.hpp"
void helper(S<int>* s)
{
	s->f(); // #1 first point of instantiation of S::f
}

// b.cpp:
#include "t.hpp"
int main()
{
	S<int> s;
	helper(&s);
	s.f(); // #2 second point of instantiation of S::f
}
\end{lstlisting}

如果链接器像处理普通函数或成员函数那样，处理类模板的成员函数实例化，编译器需要确保它只在两个POI中的一个生成代码:\#1或\#2。为了实现这一点，编译器必须将信息从一个翻译单元传递到另一个翻译单元，而在引入模板之前，C++编译器从来接到过这样的要求。在接下来的内容中，将讨论C++实现者的三大类解决方案。

模板实例化产生的所有可链接实体，都会出现同样的问题:实例化的函数模板和成员函数模板，以及实例化的静态数据成员和实例化的变量模板。

\subsubsubsection{14.4.1\hspace{0.2cm}贪婪实例化}

第一个普及贪婪实例化的C++编译器是来自一家名为Borland的公司。目前为止，已经发展成为各种C++系统中最常用的技术。

贪婪实例化假定链接器知道某些实体(特别是可链接的模板实例化)，可能在各种对象文件和库中重复出现。编译器通常会以一种特殊的方式标记这些实体。当链接器发现多个实例时，会保留一个并丢弃所有其他实例。

理论上，贪婪实例化有一些严重缺陷:

\begin{itemize}
\item 
编译器会浪费时间来生成和优化N个实例，其中只有一个会保留。

\item 
链接器通常不会检查两个实例是否相同，因为对于一个模板特化的多个实例，生成的代码中可能会出现一些无关紧要的差异。这些小的差异不会导致链接器失败。(这些差异可能是由于实例化时编译器状态的微小差异造成的。)然而，这也常常导致链接器没有注意到更实质性的差异，例如：实例化是用严格的浮点数学规则编译的，而另一个实例化是用宽松的、性能更高的浮点数学规则编译的。

\begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black]
\hspace*{0.75cm}然而，目前的系统已经发展到可以检测某些其他差异。例如，如果一个实例化有相关的调试信息，而另一个实例化没有，就会报告。
\end{tcolorbox}

\item 
所有目标文件的总和可能会比替代文件大得多，因为相同的代码可能会复制多次。
\end{itemize}

这些缺点似乎并没有造成重大问题，也许这是因为贪婪实例化的优势:保留了传统的源对象依赖关系。特别地，一个翻译单元只生成一个目标文件，每个目标文件包含对应源文件(包含实例化的定义)中所有可链接定义的编译代码。另一个重要的好处是，所有函数模板实例都是内联的候选对象，而无需求助于“链接时”优化机制(而且，函数模板实例通常受益于内联函数)。其他实例化机制专门处理内联函数模板实例，以确保它们可以内联扩展。并且，贪婪实例化允许非内联函数模板实例进行内联扩展。

最后，允许可链接实体的重复定义的链接机制，也用于处理重复溢出的内联函数

\begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black]
\hspace*{0.75cm}当编译器无法“内联”对使用关键字inline标记的函数的每次调用时，将在目标文件中产生函数的副本。这可能发生在多个目标文件中。
\end{tcolorbox}

以及虚函数调度表。

\begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black]
\hspace*{0.75cm}虚函数调用通常通过函数指针表作为间接调用实现。参见[LippmanObjMod]了解C++实现方面的深入研究。
\end{tcolorbox}

如果这种机制不可用，可以使用内部链接来发出这些项，但代价是生成更大的代码。内联函数只有一个地址的要求，使得以符合标准的方式实现该替代变得困难重重。

\subsubsubsection{14.4.2\hspace{0.2cm}查询实例化}

20世纪90年代中叶，一家名为Sun Microsystems的公司

\begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black]
\hspace*{0.75cm}Oracle公司后来收购了Sun Microsystems公司。
\end{tcolorbox}

发布了一个重新实现的C++编译器(4.0版本)，为实例化问题提供了一个新的、有趣的解决方案，我们称之为“查询实例化”。其在概念上非常简单和优雅，但它是这里回顾的最新实例化方案。这种方案中，维护由所有参与程序翻译单元编译共享的数据库。这个数据库会跟踪哪些特化已经实例化，以及它们依赖的源码。生成的特化本身会与此信息一起存储在数据库中。每当遇到可链接实体的实例化点时，可能会发生以下三种情况:

\begin{enumerate}
\item 
没有可用的特化:将发生实例化，并将特化结果输入到数据库中。

\item 
特化是可用的，但已经过时，因为生成后源就进行了更改。这里也产生了实例化，但是所产生的特化取代了以前存储在数据库中的特化。

\item 
数据库中记录了最新的特化类型。
\end{enumerate}

虽然概念上很简单，但这种设计在实现上有一些挑战:

\begin{itemize}
\item 
正确地维护与源代码状态相关数据库内容的依赖关系，并不是一件容易的事情。尽管将第三种情况误认为第二种情况并没有错，但这样做会增加编译器的工作量(也会增加整个构建时间)。

\item 
并发编译多个源文件是很常见的。因此，编译器实现需要为数据库提供适当数量的并发控制。
\end{itemize}

尽管存在一些挑战，但该方案可以有效地实施。此外，没有明显的病态的前提会使这个解决方案的扩展性很差，相反，贪婪实例化可能会导致大量的无效工作。

数据库的使用也会给开发者带来问题，其根源在于:从大多数C编译器继承的传统编译模型不再适用，单个翻译单元不再产生独立的目标文件。假设希望链接最终程序，这个链接操作不仅需要与各种翻译单元相关联的每个目标文件，还需要存储在数据库中的目标文件。类似地，如果创建二进制库，则需要确保创建该库的工具(链接器或归档器)知道数据库内容。任何对目标文件进行操作的工具，都需要知道数据库的内容。通过不将实例化存储在数据库中，而是在生成初始化的对象文件中产生目标代码，可以缓解其中的问题。

库是另一个挑战。许多生成的特化可以打包在一个库中，当库添加到另一个项目时，可能需要让该项目的数据库知道可用的实例化。如果不可用，并且项目为库中存在的特化创建了实例化点，则可能会重复的进行实例化。处理这种情况的一个策略是使用与允许贪婪实例化相同的链接器技术:让链接器知道生成的特化，并让它清除重复(发生的频率应该比贪婪实例化低得多)。源文件、目标文件和库的各种其他安排可能会导致其他问题，比如：由于包含所需实例化的目标代码，没有链接到最终的可执行程序中，而丢失实例化信息。

最终，查询实例化在市场上无法生存，甚至Sun的编译器现在也在使用贪婪实例化。

\subsubsubsection{14.4.3\hspace{0.2cm}迭代实例化}

第一个支持C++模板的编译器是Cfront 3.0，是Bjarne Stroustrup为开发该语言而编写的编译器后代。

\begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black]
\hspace*{0.75cm}不要认为Cfront是一个学术原型:它用于工业环境，并成为了许多商业C++编译器产品的基础。其3.0版本于1991年发布，但漏洞百出。此后，版本3.0.1很快就出现了，并支持模板。
\end{tcolorbox}

Cfront的一个约束是必须能够在不同平台之间移植，这意味着(1)使用C语言作为跨所有目标平台的通用目标表示，(2)使用本地目标链接器。这样链接器看不到模板，Cfront像普通C函数一样生成模板的实例化，因此必须避免重复的实例化。虽然Cfront源模型与标准包含模型不同，但它的实例化策略可以适应包含模型。因此，它是迭代实例化的第一个实现。Cfront迭代可以这样描述:

\begin{enumerate}
\item 
编译源代码时不实例化必需的可链接特化。

\item 
使用预链接器链接目标文件。

\item 
预链接器调用链接器并解析其错误消息，以确定是否有错误消息是缺少实例化的结果。如果缺少实例化，预链接器将在包含所需模板定义的源上调用编译器，并带有生成缺失实例化的选项。

\item 
若生成了定义，则重复步骤3。
\end{enumerate}

第3步中，在实例化一个可链接实体过程中，可能会要求“另一个仍未实例化”的实体进行实例化；最后，所有的迭代都已经完成，链接器才会成功创建一个完整的程序。

原始的Cfront方案的缺点相当严重:

\begin{itemize}
\item 
感知到的链接时间不仅会因为预链接器的开销而增加，而且还会因为每次重新编译和重新链接而增加。有时使用Cfront系统的用户会抱怨说:“链接时间往往需要几天”，而同样的工作，采用其他解决方案，一个小时左右就足够了。

\item 
诊断(错误、警告)延迟到连接时。当链接大型程序时，并且开发人员必须等待数小时才能找到模板定义中的错误时，这就特别的痛苦。

\item 
必须记住包含特定定义的源的位置(步骤1)。特别是Cfront使用了中央存储库，必须面对在查询实例化方法中处理中央数据库的一些挑战。特别是，最初Cfront实现并不支持并发编译。
\end{itemize}

随后，爱迪生设计集团(EDG)和惠普的aC++对迭代原理进行了改进，

\begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black]
\hspace*{0.75cm}惠普的aC++是在一家名为Taligent(后来被国际商业机器公司(IBM)收购)的技术基础上发展起来的。HP还在aC++中添加了贪婪实例化，并将其作为默认机制。
\end{tcolorbox}

避免了Cfront实现的一些缺点。实践中，这些实现工作得非常好。尽管“从头开始”构建通常比其他方案更耗时，但后续的构建时间相当有竞争力。不过，使用迭代实例化的C++编译器仍然少见。
















